use crate::core::error::operation_error::OperationError;
use crate::core::step_context::StepContext;
use regex::Regex;
use std::collections::VecDeque;
use std::sync::Arc;
use std::{fs, path::PathBuf, str::FromStr};
use tokio::{
    fs::File,
    io::{AsyncBufReadExt, AsyncWriteExt, BufReader, BufWriter},
};
use tonic::async_trait;

use super::Operation;

pub struct ReplaceText {
    search_path: Vec<PathBuf>,
    file_pattern: Option<Regex>,
    old_string_pattern: Regex,
    new_string_pattern: String,
}

async fn handle_file(p: PathBuf, old_string_pattern: Regex, new_string_pattern: String) {
    let mut result: Vec<String> = Vec::new();
    if let Ok(f) = File::options().read(true).open(&p).await {
        let buffered = BufReader::new(f);
        let mut lines = buffered.lines();
        while let Some(l) = lines.next_line().await.unwrap() {
            let nl = old_string_pattern.replace_all(&l, &*new_string_pattern);
            result.push(nl.to_string());
        }
    }

    if let Ok(f) = File::create(&p).await {
        let mut buffered = BufWriter::new(f);
        for l in result {
            buffered.write_all(l.as_bytes()).await.unwrap();
        }
    }
}

#[async_trait]
impl Operation for ReplaceText {
    async fn run(&mut self, context: Arc<StepContext>) -> Result<(), OperationError> {
        let mut tasks = Vec::new();
        let mut queue: VecDeque<PathBuf> = VecDeque::with_capacity(10);
        for p in self.search_path.iter() {
            queue.push_back(p.clone());
        }

        while let Some(p) = queue.pop_front() {
            if p.is_file() {
                if let Some(file_pattern) = &self.file_pattern {
                    if !file_pattern.is_match(p.to_str().unwrap()) {
                        continue;
                    }
                }

                let o = self.old_string_pattern.clone();
                let n = self.new_string_pattern.clone();
                let future = handle_file(p, o, n);
                let handle = tokio::spawn(future);
                tasks.push(handle);
            } else if p.is_dir() {
                fs::read_dir(p).unwrap().into_iter().for_each(|e| {
                    let p = e.unwrap().path();
                    queue.push_back(p)
                })
            }
        }

        futures::future::join_all(tasks).await;
        Ok(())
    }
}

impl ReplaceText {
    pub fn new<'a>(
        search_path: impl Iterator<Item = &'a str>,
        file_pattern: &str,
        old_string_pattern: &str,
        new_string_pattern: &str,
    ) -> Self {
        ReplaceText {
            search_path: search_path.map(|s| PathBuf::from_str(s).unwrap()).collect(),
            file_pattern: Regex::new(file_pattern).ok(),
            old_string_pattern: Regex::new(old_string_pattern).unwrap(),
            new_string_pattern: new_string_pattern.to_string(),
        }
    }
}
